require 'set'
require 'diff/lcs'
require 'tork/server'
require 'tork/config'

module Tork
class Engine < Server

  def initialize
    super
    Tork.config :engine

    @lines_by_file = {}
    @passed_test_files = Set.new
    @failed_test_files = Set.new
    @running_test_files = Set.new
    @recently_passed_test_files = Set.new
    @recently_failed_test_files = Set.new
  end

  def loop
    @master = popen('tork-master')
    super
  ensure
    pclose @master
  end

  def boot!
    @master.reconnect

    # resume running all previously running test files and
    # all previously failed test files in the new master
    resumable = @running_test_files + @failed_test_files
    @running_test_files.clear
    test resumable
  end

  def test test_file, *line_numbers
    # a list of tests was passed in for the first argument
    if test_file.respond_to? :each and line_numbers.empty?
      test_file.each {|args| test(*args) }

    elsif File.exist? test_file and @running_test_files.add? test_file
      if line_numbers.empty?
        line_numbers = find_changed_line_numbers(test_file)
      else
        line_numbers.map!(&:to_i)
        line_numbers.clear if line_numbers.any?(&:zero?)
      end
      send @master, [:test, test_file, line_numbers]
    end
  end

  def test?
    if @running_test_files.empty?
      tell @client, 'There are no running test files to list.'
    else
      tell @client, @running_test_files.sort, false
    end
  end

  def stop signal=nil
    if @running_test_files.empty?
      tell @client, 'There are no running test files to stop.'
    else
      send @master, [:stop, signal].compact
      @running_test_files.clear
    end
  end

  def pass!
    if @passed_test_files.empty?
      tell @client, 'There are no passed test files to re-run.'
    else
      test @passed_test_files
    end
  end

  def pass?
    if @passed_test_files.empty?
      tell @client, 'There are no passed test files to list.'
    else
      tell @client, @passed_test_files.sort, false
    end
  end

  def fail!
    if @failed_test_files.empty?
      tell @client, 'There are no failed test files to re-run.'
    else
      test @failed_test_files
    end
  end

  def fail?
    if @failed_test_files.empty?
      tell @client, 'There are no failed test files to list.'
    else
      tell @client, @failed_test_files.sort, false
    end
  end

protected

  def recv client, message
    case client
    when @master
      send @clients, message # propagate downstream

      event, file, line_numbers = message
      case event_sym = event.to_sym
      when :fail, :pass
        @running_test_files.delete file

        if event_sym == :fail
          @recently_failed_test_files.add file
          was_pass = @passed_test_files.delete? file
          now_fail = @failed_test_files.add? file
          send @clients, [:fail!, file, message] if was_pass and now_fail

        elsif line_numbers.empty?
          # only whole test file runs should qualify as pass
          @recently_passed_test_files.add file
          was_fail = @failed_test_files.delete? file
          now_pass = @passed_test_files.add? file
          send @clients, [:pass!, file, message] if was_fail and now_pass
        end

        # notify user when all test files have finished running
        if @running_test_files.empty?
          passed = @recently_passed_test_files.to_a
          @recently_passed_test_files.clear

          failed = @recently_failed_test_files.to_a
          @recently_failed_test_files.clear

          tested = passed + failed
          send @clients, [:done, tested, passed, failed]
        end
      end

    else
      super
    end
  end

private

  def find_changed_line_numbers test_file
    # cache test file contents for diffing below
    new_lines = File.readlines(test_file)
    old_lines = @lines_by_file[test_file] || new_lines
    @lines_by_file[test_file] = new_lines

    # find changed line numbers in the test file
    Diff::LCS.diff(old_lines, new_lines).flatten.
      # +1 because line numbers start at 1, not 0
      map {|change| change.position + 1 }.uniq
  end

end
end
